<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style>
			#canvas {
				border: 1px solid red;
			}
		</style>
	</head>
	<body>
		<canvas id="canvas" width="150" height="150"></canvas>
		<button id="saveBtn">保存</button>
		<button id="clearBtn">清空</button>
		<button id="emptyBtn">是否为空</button>
		<button id="eraserBtn">使用橡皮擦</button>
		<button id="rectBtn">使用矩形</button>
		<script type="text/javascript">
			class DrawBoard {
				// 公开变量
				// 橡皮擦
				eraserEnabled = false;
				// 矩形
				rectEnabled = false;
				// 下划线为内部变量
				// 是否开始画的记录
				_painting = false;
				// 画线起点记录
				_last;
				_canvas;
				_ctx;
				// 上层画布
				_upperCanvas;
				_upperCtx;
				_eraserSize; // 橡皮差大小
				// 空画布，当isJudgeEmpty为true时使用
				_emptyDrawBoard;
				
				constructor(domID, {
					fillStyle = "black",
					strokeStyle = 'black',
					lineWidth = 2,
					isJudgeEmpty = false
				} = {}) {
					this._canvas = document.getElementById(domID);
					this._ctx = this._canvas.getContext('2d');
					this.#initUpperCanvas();
					this._ctx.fillStyle = fillStyle;
					this._ctx.strokeStyle = strokeStyle
					this._ctx.lineWidth = lineWidth;
					this._ctx.lineCap = "round"; //设置线条末端样式
					this.#initEvent();
					if (isJudgeEmpty) setTimeout(() => this._emptyDrawBoard = this.toDataURL());
				}
				// 初始化事件
				#initEvent() {
					const isTouchDevice = 'ontouchstart' in document.documentElement;
					const events = isTouchDevice ? ["touchstart", "touchmove", "touchend"] : ["mousedown", "mousemove", "mouseup"];
					events.forEach(item => this._upperCanvas.addEventListener(item, (e) => this[item](e)));
				}
				// 触屏
				touchstart(f) {
					let x = f.touches[0].clientX;
					let y = f.touches[0].clientY;
					this._last = [x, y];
				}
				// 手移动时
				touchmove(f) {
					let x = f.touches[0].clientX;
					let y = f.touches[0].clientY;
					if (this.eraserEnabled) this.#useEraser(x, y);
					else this.#drawLine(this._last[0], this._last[1], x, y);
					this._last = [x, y];
				}
				// 触摸结束
				touchend() {
					this._painting = false;
					this.#upperClear();
				}
				// 输入按下时
				mousedown(e) {
				    this._painting = true;
					// 使用offset鼠标最准确
					this._last = [e.offsetX, e.offsetY];
				}
				// 鼠标移动时
				mousemove(e) {
					if (this._painting) {
						const { offsetX: x, offsetY: y } = e;
						// 启用矩形绘画时画到上层画布，但绘画完成再绘画到目标画布
						if (this.rectEnabled) {
							this.#upperClear();
							return this.#drawRect(this._upperCtx, x, y);
						}
						// 是否启用橡皮差
						if (this.eraserEnabled) this.#useEraser(x, y);
						else this.#drawLine(this._last[0], this._last[1], x, y);
						this._last = [x, y]; // 记录上一个点
					}
				}
				// 鼠标抬起时
				mouseup(e) {
				  this._painting = false;
				  this.#upperClear();
				  if (this.rectEnabled) {
					  const { offsetX: x, offsetY: y } = e;
					  this.#drawRect(this._ctx, x, y);
				  }
				}
				// 画线
				#drawLine(x1, y1, x2, y2) {
					this._ctx.beginPath();
					this._ctx.moveTo(x1, y1);
					this._ctx.lineTo(x2, y2);
					this._ctx.closePath();
					this._ctx.stroke();
				}
				#drawRect(ctx, offsetX, offsetY) {
					const [x, y] = this._last;
					const w = offsetX - x;
					const h = offsetY - y;
					ctx.strokeRect(x, y, w, h);
				}
				// 写入高度
				set height(value) {
					this._canvas.height = value;
					this._upperCanvas.height = value;
				}
				// 写入宽度
				set width(value) {
					this._canvas.width = value;
					this._upperCanvas.width = value;
				}
				// 启用橡皮擦
				enableEraser(size = 30) {
					this.eraserEnabled = true;
					this._eraserSize = size;
				}
				// 禁用橡皮擦
				disabledEraser() {
					this.eraserEnabled = false;
				}
				// 使用橡皮擦
				#useEraser(x, y) {
					const offset = this._eraserSize / 2;
					// 实际清除
					this._ctx.clearRect(x - offset, y - offset, this._eraserSize, this._eraserSize);
					// 控制橡皮差移动
					this.#upperClear();
					this._upperCtx.beginPath();//开始绘制
					this._upperCtx.arc(this._last[0], this._last[1], this._eraserSize / 2, 0, 2*Math.PI);//arc 的意思是“弧”
					this._upperCtx.strokeStyle="#000";
					this._upperCtx.stroke();//stroke() 方法默认颜色是黑色（如果没有上面一行，则会是黑色）。
				}
				// 初始化上层画布用来做显示逻辑
				#initUpperCanvas() {
					this._upperCanvas = document.createElement("canvas");
					this._upperCtx = this._upperCanvas.getContext('2d');
					const { x, y, width, height } = this._canvas.getBoundingClientRect();
					this._upperCanvas.width = width;
					this._upperCanvas.height = height;
					this._upperCanvas.style.position = "absolute";
					this._upperCanvas.style.left = x + "px";
					this._upperCanvas.style.top = y + "px";
					document.body.appendChild(this._upperCanvas);
				}
				// 清空上层画布
				#upperClear() {
					const { width, height } = this._upperCanvas;
					this._upperCtx.clearRect(0, 0, width, height);
				}
				// 生成图片
				toDataURL(bgColor) {
					if (bgColor) {
						const [r, g, b, a] = this.#convertColor(bgColor);
						const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
						for (let i = 0; i < imageData.data.length; i += 4) {
						  // 当该像素是透明的，则设置成白色
						  if (imageData.data[i + 3] === 0) {
							imageData.data[i] = r;
							imageData.data[i + 1] = g;
							imageData.data[i + 2] = b;
							imageData.data[i + 3] = a;
						  }
						}
						this._ctx.putImageData(imageData, 0, 0);
					}
					return this._canvas.toDataURL('image/png');
				}
				// 16色转rgba
				#convertColor(color) {
					// 提取红色通道，并将其从16进制转换为10进制
					const red = parseInt(color.substr(1, 2) || 0, 16); 
					// 提取绿色通道
					const green = parseInt(color.substr(3, 2) || 0, 16); 
					// 提取蓝色通道
					const blue = parseInt(color.substr(5, 2) || 0, 16);
					// 提取透明度通道
					const alpha = parseInt(color.substr(7, 2) || "ff", 16); 
					// 将结果以RGBA格式返回
					return [red, green, blue, alpha];
				}

				// 清空画板
				clear() {
					const { width, height } = this._canvas;
					this._ctx.clearRect(0, 0, width, height);
				}
				// 是否为空，isJudgeEmpty 不启用时恒为 false
				isEmpty() {
					return this.toDataURL() === this._emptyDrawBoard;
				}
				
			}
			
			const drawBoard = new DrawBoard("canvas", { isJudgeEmpty: true });
			drawBoard.width = document.documentElement.clientWidth - 50; //网页可见区域宽
			drawBoard.height = document.documentElement.clientHeight - 100; //网页可见区域高
			
			saveBtn.onclick = () => {
				let url = drawBoard.toDataURL();
				let a = document.createElement('a');
				a.href = url;
				a.download = '草稿纸';
				a.target = '_blank';
				a.click();
			}
			clearBtn.onclick = () => {
				drawBoard.clear();
			}
			emptyBtn.onclick = () => {
				console.log(drawBoard.isEmpty());
			}
			eraserBtn.onclick = () => {
				if (drawBoard.eraserEnabled) drawBoard.disabledEraser(); 
				else drawBoard.enableEraser();
				eraserBtn.innerText = drawBoard.eraserEnabled ? "取消橡皮擦" : "使用橡皮擦"
			}
			rectBtn.onclick = () => {
				drawBoard.rectEnabled = true
			}
		</script>
	</body>
</html>